/**
* Alipay.com Inc.
* Copyright (c) 2004-2012 All Rights Reserved.
*/
package com.alipay.zdal.datasource.resource.connectionmanager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.management.ObjectName;
import javax.security.auth.Subject;
import javax.transaction.xa.XAException;
import javax.transaction.xa.XAResource;
import javax.transaction.xa.Xid;
import org.apache.log4j.Logger;
import com.alipay.zdal.datasource.resource.JBossResourceException;
import com.alipay.zdal.datasource.resource.ResourceException;
import com.alipay.zdal.datasource.resource.spi.ConnectionEvent;
import com.alipay.zdal.datasource.resource.spi.ConnectionRequestInfo;
import com.alipay.zdal.datasource.resource.spi.LocalTransaction;
import com.alipay.zdal.datasource.resource.spi.ManagedConnection;
import com.alipay.zdal.datasource.resource.util.NestedRuntimeException;
import com.alipay.zdal.datasource.tm.LastResource;
import com.alipay.zdal.datasource.tm.TransactionTimeoutConfiguration;
import com.alipay.zdal.datasource.tm.TxUtils;
import com.alipay.zdal.datasource.transaction.RollbackException;
import com.alipay.zdal.datasource.transaction.Status;
import com.alipay.zdal.datasource.transaction.Synchronization;
import com.alipay.zdal.datasource.transaction.SystemException;
import com.alipay.zdal.datasource.transaction.Transaction;
import com.alipay.zdal.datasource.transaction.TransactionManager;
/**
* The TxConnectionManager is a JBoss ConnectionManager
* implementation for jca adapters implementing LocalTransaction and XAResource support.
*
* It implements a ConnectionEventListener that implements XAResource to
* manage transactions through the Transaction Manager. To assure that all
* work in a local transaction occurs over the same ManagedConnection, it
* includes a xid to ManagedConnection map. When a Connection is requested
* or a transaction started with a connection handle in use, it checks to
* see if a ManagedConnection already exists enrolled in the global
* transaction and uses it if found. Otherwise a free ManagedConnection
* has its LocalTransaction started and is used. From the
* BaseConnectionManager2, it includes functionality to obtain managed
* connections from
* a ManagedConnectionPool mbean, find the Subject from a SubjectSecurityDomain,
* and interact with the CachedConnectionManager for connections held over
* transaction and method boundaries. Important mbean references are to a
* ManagedConnectionPool supplier (typically a JBossManagedConnectionPool), and a
* RARDeployment representing the ManagedConnectionFactory.
*
* This connection manager has to perform the following operations:
*
* 1. When an application component requests a new ConnectionHandle,
* it must find a ManagedConnection, and make sure a
* ConnectionEventListener is registered. It must inform the
* CachedConnectionManager that a connection handle has been given
* out. It needs to count the number of handles for each
* ManagedConnection. If there is a current transaction, it must
* enlist the ManagedConnection's LocalTransaction in the transaction
* using the ConnectionEventListeners XAResource XAResource implementation.
* Entry point: ConnectionManager.allocateConnection.
* written.
*
* 2. When a ConnectionClosed event is received from the
* ConnectionEventListener, it must reduce the handle count. If
* the handle count is zero, the XAResource should be delisted from
* the Transaction, if any. The CachedConnectionManager must be
* notified that the connection is closed.
* Entry point: ConnectionEventListener.ConnectionClosed.
* written
*
*3. When a transaction begun notification is received from the
* UserTransaction (via the CachedConnectionManager, all
* managedConnections associated with the current object must be
* enlisted in the transaction.
* Entry point: (from
* CachedConnectionManager)
* ConnectionCacheListener.transactionStarted(Transaction,
* Collection). The collection is of ConnectionRecord objects.
* written.
*
* 5. When an "entering object" notification is received from the
* CachedConnectionInterceptor, all the connections for the current
* object must be associated with a ManagedConnection. if there is a
* Transaction, the XAResource must be enlisted with it.
* Entry point: ConnectionCacheListener.reconnect(Collection conns) The Collection
* is of ConnectionRecord objects.
* written.
*
* 6. When a "leaving object" notification is received from the
* CachedConnectionInterceptor, all the managedConnections for the
* current object must have their XAResources delisted from the
* current Transaction, if any, and cleanup called on each
* ManagedConnection.
* Entry point: ConnectionCacheListener.disconnect(Collection conns).
* written.
*
* @author ����
* @version $Id: TxConnectionManager.java, v 0.1 2014-1-6 ����05:36:35 Exp $
*/
public class TxConnectionManager extends BaseConnectionManager2 {
private static final Logger log = Logger
.getLogger(TxConnectionManager.class);
private static final Throwable FAILED_TO_ENLIST = new Throwable(
"Unabled to enlist resource, see the previous warnings.");
private ObjectName transactionManagerService;
private TransactionManager tm;
private boolean trackConnectionByTx = false;
private boolean localTransactions;
private int xaResourceTimeout = 0;
private Boolean isSameRMOverrideValue;
protected static void rethrowAsSystemException(String context, Transaction tx, Throwable t)
throws SystemException {
if (t instanceof SystemException)
throw (SystemException) t;
if (t instanceof RuntimeException)
throw (RuntimeException) t;
if (t instanceof Error)
throw (Error) t;
if (t instanceof RollbackException)
throw new IllegalStateException(context + " tx=" + tx + " marked for rollback.");
throw new NestedRuntimeException(context + " tx=" + tx + " got unexpected error ", t);
}
/**
* Default managed TxConnectionManager constructor for mbean instances.
*/
public TxConnectionManager() {
}
/**
* Creates a new <code>TxConnectionManager</code> instance.
* for TESTING ONLY!!! not a managed constructor!!
*
* @param ccm a <code>CachedConnectionManager</code> value
* @param poolingStrategy a <code>ManagedConnectionPool</code> value
* @param tm a <code>TransactionManager</code> value
*/
public TxConnectionManager(final CachedConnectionManager ccm,
final ManagedConnectionPool poolingStrategy,
final TransactionManager tm) {
super(ccm, poolingStrategy);
this.tm = tm;
}
public ObjectName getTransactionManagerService() {
return transactionManagerService;
}
public void setTransactionManagerService(ObjectName transactionManagerService) {
this.transactionManagerService = transactionManagerService;
}
@Override
public TransactionManager getTransactionManagerInstance() {
return tm;
}
public void setTransactionManagerInstance(TransactionManager tm) {
this.tm = tm;
}
public boolean isTrackConnectionByTx() {
return trackConnectionByTx;
}
public void setTrackConnectionByTx(boolean trackConnectionByTx) {
this.trackConnectionByTx = trackConnectionByTx;
}
public boolean isLocalTransactions() {
return localTransactions;
}
public void setLocalTransactions(boolean localTransactions) {
this.localTransactions = localTransactions;
}
public int getXAResourceTransactionTimeout() {
return xaResourceTimeout;
}
public void setXAResourceTransactionTimeout(int timeout) {
this.xaResourceTimeout = timeout;
}
public Boolean getIsSameRMOverrideValue() {
return isSameRMOverrideValue;
}
public void setIsSameRMOverrideValue(Boolean isSameRMOverride) {
this.isSameRMOverrideValue = isSameRMOverride;
}
@Override
public long getTimeLeftBeforeTransactionTimeout(boolean errorRollback) throws RollbackException {
if (tm == null)
throw new IllegalStateException("No transaction manager");
if (tm instanceof TransactionTimeoutConfiguration)
return ((TransactionTimeoutConfiguration) tm)
.getTimeLeftBeforeTransactionTimeout(errorRollback);
return -1;
}
@Override
public ConnectionListener getManagedConnection(Subject subject, ConnectionRequestInfo cri)
throws ResourceException {
Transaction trackByTransaction = null;
try {
Transaction tx = tm.getTransaction();
if (tx != null && TxUtils.isActive(tx) == false) {
throw new ResourceException("Transaction is not active: tx=" + tx);
}
if (trackConnectionByTx) {
trackByTransaction = tx;
}
} catch (Throwable t) {
JBossResourceException.rethrowAsResourceException("Error checking for a transaction.",
t);
}
return super.getManagedConnection(trackByTransaction, subject, cri);
}
@Override
public void transactionStarted(Collection crs) throws SystemException {
Set cls = new HashSet();
for (Iterator i = crs.iterator(); i.hasNext();) {
ConnectionRecord cr = (ConnectionRecord) i.next();
ConnectionListener cl = cr.cl;
if (!cls.contains(cl)) {
cls.add(cl);
cl.enlist();
}
}
}
@Override
protected void managedConnectionReconnected(ConnectionListener cl) throws ResourceException {
try {
cl.enlist();
} catch (Throwable t) {
throw new JBossResourceException(
"Could not enlist in transaction on entering meta-aware object!", t);
}
}
@Override
protected void managedConnectionDisconnected(ConnectionListener cl) throws ResourceException {
Throwable throwable = null;
try {
cl.delist();
} catch (Throwable t) {
throwable = t;
}
//if there are no more handles and tx is complete, we can return to pool.
boolean isFree = cl.isManagedConnectionFree();
if (isFree) {
returnManagedConnection(cl, false);
}
// Rethrow the error
if (throwable != null)
JBossResourceException.rethrowAsResourceException(
"Could not delist resource, probably a transaction rollback? ", throwable);
}
public ConnectionListener createConnectionListener(ManagedConnection mc, Object context)
throws ResourceException {
XAResource xaResource = null;
if (localTransactions) {
xaResource = new LocalXAResource(log);
// if (log.isDebugEnabled() && xaResourceTimeout != 0) {
// log.debug("XAResource transaction timeout cannot be set for local transactions: "
// + getName());
// }
} else {
throw new UnsupportedOperationException("Support local transactions only");
// xaResource = JcaXAResourceWrapperFactory.getResourceWrapper(mc.getXAResource(), isSameRMOverrideValue);
//
// if (xaResourceTimeout != 0)
// {
// try
// {
// if (xaResource.setTransactionTimeout(xaResourceTimeout) == false)
// log.debug("XAResource does not support transaction timeout configuration: " + getName());
// }
// catch (XAException e)
// {
// throw new JBossResourceException("Unable to set XAResource transaction timeout: " + getName(), e);
// }
// }
}
ConnectionListener cli = new TxConnectionEventListener(mc, poolingStrategy, context, log,
xaResource);
mc.addConnectionEventListener(cli);
return cli;
}
@Override
public boolean isTransactional() {
return TxUtils.isCompleted(tm) == false;
}
// implementation of javax.resource.spi.ConnectionEventListener interface
//there is one of these for each ManagedConnection instance. It lives as long as the ManagedConnection.
protected class TxConnectionEventListener extends
BaseConnectionManager2.BaseConnectionEventListener {
/** Use our own logger to prevent MNFE caused by compiler bug with nested classes. */
protected Logger log;
protected TransactionSynchronization transactionSynchronization;
private final XAResource xaResource;
/** Whether there is a local transaction */
private final AtomicBoolean localTransaction = new AtomicBoolean(false);
public TxConnectionEventListener(final ManagedConnection mc,
final ManagedConnectionPool mcp, final Object context,
Logger log, final XAResource xaResource)
throws ResourceException {
super(mc, mcp, context, log);
this.log = log;
this.xaResource = xaResource;
if (xaResource instanceof LocalXAResource)
((LocalXAResource) xaResource).setConnectionListener(this);
}
@Override
public void enlist() throws SystemException {
// This method is a bit convulted, but it has to be such because
// there is a race condition in the transaction manager where it
// unlocks during the enlist of the XAResource. It does this
// to avoid distributed deadlocks and to ensure the transaction
// timeout can fail a badly behaving resource during the enlist.
//
// When two threads in the same transaction are trying to enlist
// connections they could be from the same resource manager
// or even the same connection when tracking the connection by transaction.
//
// For the same connection, we only want to do the real enlist once.
// For two connections from the same resource manager we don't
// want the join before the initial start request.
//
// The solution is to build up a list of unenlisted resources
// in the TransactionSynchronizer and then choose one of the
// threads that is contending in the transaction to enlist them
// in order. The actual order doesn't really matter as it is the
// transaction manager that calculates the enlist flags and determines
// whether the XAResource was already enlisted.
//
// Once there are no unenlisted resources the threads are released
// to return the result of the enlistments.
//
// In practice, a thread just takes a snapshot to try to avoid one
// thread having to do all the work. If it did not do them all
// the next waiting thread will do the next snapshot until there
// there is either no snapshot or no waiting threads.
//
// A downside to this design is a thread could have its resource enlisted by
// an earlier thread while it enlists some later thread's resource.
// Since they are all a part of the same transaction, this is probably
// not a real issue.
// No transaction associated with the thread
int status = tm.getStatus();
if (status == Status.STATUS_NO_TRANSACTION) {
if (transactionSynchronization != null
&& transactionSynchronization.currentTx != null) {
String error = "Attempt to use connection outside a transaction when already a tx!";
throw new IllegalStateException(error);
}
return;
}
// Inactive transaction
Transaction threadTx = tm.getTransaction();
if (threadTx == null || status != Status.STATUS_ACTIVE) {
String error = "Transaction " + threadTx + " is not active "
+ TxUtils.getStatusAsString(status);
throw new IllegalStateException(error);
}
// Our synchronization
TransactionSynchronization ourSynchronization = null;
// Serializes enlistment when two different threads are enlisting
// different connections in the same transaction concurrently
TransactionSynchronizer synchronizer = null;
TransactionSynchronizer.lock(threadTx);
try {
// Interleaving should have an unenlisted transaction
// TODO: We should be able to do some sharing shouldn't we?
if (isTrackByTx() == false && transactionSynchronization != null) {
String error = "Can't enlist - already a tx!";
throw new IllegalStateException(error);
}
// Check for different transaction
if (transactionSynchronization != null
&& transactionSynchronization.currentTx.equals(threadTx) == false) {
String error = "Trying to change transaction " + threadTx + " in enlist!";
throw new IllegalStateException(error);
}
// Get the synchronizer
try {
synchronizer = TransactionSynchronizer.getRegisteredSynchronizer(threadTx);
} catch (Throwable t) {
setTrackByTx(false);
rethrowAsSystemException("Cannot register synchronization", threadTx, t);
}
// First time through, create a transaction synchronization
if (transactionSynchronization == null) {
TransactionSynchronization synchronization = new TransactionSynchronization(
threadTx, isTrackByTx());
synchronizer.addUnenlisted(synchronization);
transactionSynchronization = synchronization;
}
ourSynchronization = transactionSynchronization;
} finally {
TransactionSynchronizer.unlock(threadTx);
}
// Perform the enlistment(s)
ArrayList unenlisted = synchronizer.getUnenlisted();
if (unenlisted != null) {
try {
for (int i = 0; i < unenlisted.size(); ++i) {
TransactionSynchronization sync = (TransactionSynchronization) unenlisted
.get(i);
if (sync.enlist())
synchronizer.addEnlisted(sync);
}
} finally {
synchronizer.enlisted();
}
}
ourSynchronization.checkEnlisted();
}
@Override
public void delist() throws ResourceException {
try {
if (isTrackByTx() == false && transactionSynchronization != null) {
Transaction tx = transactionSynchronization.currentTx;
TransactionSynchronization synchronization = transactionSynchronization;
transactionSynchronization = null;
if (TxUtils.isUncommitted(tx)) {
TransactionSynchronizer synchronizer = TransactionSynchronizer
.getRegisteredSynchronizer(tx);
if (synchronization.enlisted)
synchronizer.removeEnlisted(synchronization);
if (tx.delistResource(getXAResource(), XAResource.TMSUSPEND) == false)
throw new ResourceException("Failure to delist resource: " + this);
}
}
} catch (Throwable t) {
JBossResourceException.rethrowAsResourceException("Error in delist!", t);
}
}
//local will return this, xa will return one from mc.
protected XAResource getXAResource() {
return xaResource;
}
public void connectionClosed(ConnectionEvent ce) {
if (this.getManagedConnection() != (ManagedConnection) ce.getSource())
throw new IllegalArgumentException(
"ConnectionClosed event received from wrong ManagedConnection! Expected: "
+ this.getManagedConnection() + ", actual: " + ce.getSource());
try {
getCcm().unregisterConnection(TxConnectionManager.this, ce.getConnectionHandle());
} catch (Throwable t) {
log.warn(t);
}
try {
unregisterAssociation(this, ce.getConnectionHandle());
boolean isFree = isManagedConnectionFree();
//no more handles
if (isFree) {
delist();
returnManagedConnection(this, false);
}
} catch (Throwable t) {
log.error("Error while closing connection handle!", t);
returnManagedConnection(this, true);
}
}
public void localTransactionStarted(ConnectionEvent ce) {
localTransaction.set(true);
}
public void localTransactionCommitted(ConnectionEvent ce) {
localTransaction.set(false);
}
public void localTransactionRolledback(ConnectionEvent ce) {
localTransaction.set(false);
}
@Override
public void tidyup() throws ResourceException {
// We have a hanging transaction
if (localTransaction.get()) {
LocalTransaction local = null;
ManagedConnection mc = getManagedConnection();
try {
local = mc.getLocalTransaction();
} catch (Throwable t) {
JBossResourceException.rethrowAsResourceException(
"Unfinished local transaction - error getting local transaction from "
+ this, t);
}
if (local == null)
throw new ResourceException(
"Unfinished local transaction but managed connection does not provide a local transaction. "
+ this);
else {
local.rollback();
// if (log.isDebugEnabled()) {
// log.debug("Unfinished local transaction was rolled back." + this);
// }
}
}
}
@Override
public void connectionErrorOccurred(ConnectionEvent ce) {
transactionSynchronization = null;
super.connectionErrorOccurred(ce);
}
//Important method!!
@Override
public boolean isManagedConnectionFree() {
if (isTrackByTx() && transactionSynchronization != null)
return false;
return super.isManagedConnectionFree();
}
private class TransactionSynchronization implements Synchronization {
/** Transaction */
private final Transaction currentTx;
/** This is the status when we were registered */
private final boolean wasTrackByTx;
/** Whether we are enlisted */
private boolean enlisted = false;
/** Any error during enlistment */
private Throwable enlistError;
/**
* Create a new TransactionSynchronization.
*
* @param trackByTx whether this is track by connection
*/
public TransactionSynchronization(Transaction tx, boolean trackByTx) {
this.currentTx = tx;
wasTrackByTx = trackByTx;
}
/**
* Get the result of the enlistment
*
* @throws SystemExeption for any error
*/
public void checkEnlisted() throws SystemException {
if (enlistError != null) {
String error = "Error enlisting resource in transaction=" + currentTx;
// Wrap the error to give a reasonable stacktrace since the resource
// could have been enlisted by a different thread
if (enlistError == FAILED_TO_ENLIST)
throw new SystemException(FAILED_TO_ENLIST + " tx=" + currentTx);
else {
SystemException e = new SystemException(error);
e.initCause(enlistError);
throw e;
}
}
if (enlisted == false) {
throw new IllegalStateException("Resource was not enlisted.");
}
}
/**
* Enlist the resource
*
* @return true when enlisted, false otherwise
*/
public boolean enlist() {
try {
XAResource resource = getXAResource();
if (false == currentTx.enlistResource(resource))
enlistError = FAILED_TO_ENLIST;
} catch (Throwable t) {
enlistError = t;
}
synchronized (this) {
if (enlistError != null) {
setTrackByTx(false);
transactionSynchronization = null;
return false;
}
enlisted = true;
return true;
}
}
public void beforeCompletion() {
}
public void afterCompletion(int status) {
// The connection got destroyed during the transaction
if (getState() == DESTROYED)
return;
// Are we still in the original transaction?
if (this.equals(transactionSynchronization) == false) {
// If we are interleaving transactions we have nothing to do
if (wasTrackByTx == false)
return;
else {
// There is something wrong with the pooling
String message = "afterCompletion called with wrong tx! Expected: " + this
+ ", actual: " + transactionSynchronization;
IllegalStateException e = new IllegalStateException(message);
log.error("There is something wrong with the pooling?", e);
}
}
// "Delist"
transactionSynchronization = null;
// This is where we close when doing track by transaction
if (wasTrackByTx) {
setTrackByTx(false);
if (isManagedConnectionFree())
returnManagedConnection(TxConnectionEventListener.this, false);
}
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("TxSync").append(System.identityHashCode(this));
buffer.append("{tx=").append(currentTx);
buffer.append(" wasTrackByTx=").append(wasTrackByTx);
buffer.append(" enlisted=").append(enlisted);
buffer.append("}");
return buffer.toString();
}
}
// For debugging
@Override
protected void toString(StringBuffer buffer) {
buffer.append(" xaResource=").append(xaResource);
buffer.append(" txSync=").append(transactionSynchronization);
}
}
private class LocalXAResource implements XAResource, LastResource {
protected Logger log;
private ConnectionListener cl;
/**
* <code>warned</code> is set after one warning about a local participant
* in a multi-branch jta transaction is logged.
*
*/
private boolean warned = false;
private Xid currentXid;
public LocalXAResource(final Logger log) {
this.log = log;
}
void setConnectionListener(ConnectionListener cl) {
this.cl = cl;
}
// implementation of javax.transaction.xa.XAResource interface
public void start(Xid xid, int flags) throws XAException {
if (log.isDebugEnabled()) {
log.debug("start, xid: " + xid + ", flags: " + flags);
}
if (currentXid != null && flags == XAResource.TMNOFLAGS)
throw new JBossLocalXAException(
"Trying to start a new tx when old is not complete! old: " + currentXid
+ ", new " + xid + ", flags " + flags, XAException.XAER_PROTO);
if (currentXid == null && flags != XAResource.TMNOFLAGS)
throw new JBossLocalXAException("Trying to start a new tx with wrong flags! new "
+ xid + ", flags " + flags, XAException.XAER_PROTO);
if (currentXid == null) {
try {
cl.getManagedConnection().getLocalTransaction().begin();
} catch (ResourceException re) {
throw new JBossLocalXAException("Error trying to start local tx: ",
XAException.XAER_RMERR, re);
} catch (Throwable t) {
throw new JBossLocalXAException("Throwable trying to start local transaction!",
XAException.XAER_RMERR, t);
}
currentXid = xid;
}
}
public void end(Xid xid, int flags) throws XAException {
if (log.isDebugEnabled()) {
log.debug("end on xid: " + xid + " called with flags " + flags);
}
}
public void commit(Xid xid, boolean onePhase) throws XAException {
if (xid.equals(currentXid) == false)
throw new JBossLocalXAException("wrong xid in commit: expected: " + currentXid
+ ", got: " + xid, XAException.XAER_PROTO);
currentXid = null;
try {
cl.getManagedConnection().getLocalTransaction().commit();
} catch (ResourceException re) {
returnManagedConnection(cl, true);
if (log.isDebugEnabled()) {
log.debug("commit problem: ", re);
}
throw new JBossLocalXAException("could not commit local tx",
XAException.XA_RBROLLBACK, re);
}
}
public void forget(Xid xid) throws XAException {
throw new JBossLocalXAException("forget not supported in local tx",
XAException.XAER_RMERR);
}
public int getTransactionTimeout() throws XAException {
// TODO: implement this javax.transaction.xa.XAResource method
return 0;
}
public boolean isSameRM(XAResource xaResource) throws XAException {
return xaResource == this;
}
public int prepare(Xid xid) throws XAException {
if (!warned)
log
.warn("Prepare called on a local tx. Use of local transactions on a jta transaction with more than one branch may result in inconsistent data in some cases of failure.");
warned = true;
return XAResource.XA_OK;
}
public Xid[] recover(int flag) throws XAException {
throw new JBossLocalXAException("no recover with local-tx only resource managers",
XAException.XAER_RMERR);
}
public void rollback(Xid xid) throws XAException {
if (xid.equals(currentXid) == false)
throw new JBossLocalXAException("wrong xid in rollback: expected: " + currentXid
+ ", got: " + xid, XAException.XAER_PROTO);
currentXid = null;
try {
cl.getManagedConnection().getLocalTransaction().rollback();
} catch (ResourceException re) {
returnManagedConnection(cl, true);
if (log.isDebugEnabled()) {
log.debug("rollback problem: ", re);
}
throw new JBossLocalXAException("could not rollback local tx",
XAException.XAER_RMERR, re);
}
}
public boolean setTransactionTimeout(int seconds) throws XAException {
// TODO: implement this javax.transaction.xa.XAResource method
return false;
}
}
}